My mistakes in GUI design
At Assembly'03, Chaos/Farbrausch gave a very interesting seminar about the history of their Werkkzeug tool, and gave plenty of tips for those wanting to build their own demo-editors. In the section "About GUI Design", one tip was "don't use Windows, code your own GUI-library! (In the end, it will save a lot of time)." That was advice I really liked, since I'm the "make your own tools from scratch" type of coder (to a fault).
In the following years while coding my own UI, I have often wondered if that advice was Farbrausch's secret attempt to delay everyone so they can rule the demoscene :) It is indeed very easy to make an extremely basic UI component, say a button that calls a function when you click it. However, when you try to make a more-or-less complete UI, things get hairy very quickly. Drag and drop is much harder to do. You get tired of your own ugly bitmapped font, so you try to add truetype font support, which breaks your all your text-editing widgets, since truetype fonts can be proportional and your own font was fixed-width. Components are connected in ways you didn't expect, and suddenly clicking a button causes the whole UI (including that button whose code is in the stack trace) to be deleted and constructed anew. The people that use your programs nag that the buttons do their action on click instead of release, that copy-paste doesn't work, that you can't undo changes in edit fields, etc., etc.
So my GUI code really needed a rewrite, and since I don't have that much time/motivation at home, I hoped to get started with it at Breakpoint (something in the demoparty atmosphere always motivates me :) ) Lo and behold: there was a seminar on Friday from Chaos/Farbrausch, titled: "Coding User Interfaces from Scratch". It was once again very interesting, and I left with the feeling I had made some stupid beginner mistakes because I simply hadn't enough UI experience when I started. Chaos kindly gave permission to distribute his seminar slides, so I'll go over some of them and add comments about my mistakes (only the first 12 pages deal with UI, the rest is more generic stuff I can't add anything useful to). I suggest you open the slides in a second window while reading this article; they're in the bonus pack of this issue. The seminar was taped but the recording machine crashed, so no one's sure if it can be recovered.
Page 3: Chaos toned down his old "it will save a lot of time" comment to "let's call it a draw". My feeling is that unless you've coded UIs before, you're probably better off using an existing UI. Only when you run into its limitations should you think about rolling your own. You might have to adapt quite a bit of code at that point, but at least you'll know 1) how complex a UI can be and 2) which features you need and which you don't.
Page 4: DX or GDI. Very interesting page, with two things I didn't know: DirectX buffers frames, which causes lagging mouse/keyboard input, and DirectX is meant for "large" applications -- it uses limited hardware resources, so you don't want many DirectX programs to be running simultaneously. If you want to make small tools which run all the time, such as an MP3 player, DirectX might not be the best choice.
I use OpenGL in combination with SDL, so my code is cross-platform should I ever jump the Microsoft-ship. I was curious if that limitation also applied to OpenGL, so I made a bunch of copies of my simplest program, gave them a unique name and tried to launch them all. FRAPS was running since I expected to see sudden drops in the frame rate, but when launching the 5th one, they ALL crashed :( I asked SDL to use hardware acceleration, and I did only basic error-checking with no fallback strategy...
To fix this problem, I've started to redesign my UI to allow multiple drawing methods: software rendering and OpenGL for the moment, but I can probably support DirectX with little effort should I have to. The key to the solution is that most sane UI components are drawn by a very small group of primitives: horizontal and vertical lines, filled rectangles, and rectangular textures (for fonts f.e.). So instead of making a class OpenGLButton (Yes I use C++) that uses glBegin(GL_LINES) calls and such to draw itself, I use a generic GUIButton class that calls DrawingMethod::GetInstance()->Line(). DrawingMethod is a singleton class (meaning there is only 1 instance of it in use at once) that defines the primitive drawing operations as pure virtual calls:
class DrawingMethod
{
public:
static DrawingMethod* GetInstance() { return mInstance; }
static void SetInstance( DrawingMethod* inInstance);
// primitives
virtual void Point( int32 x, int32 y, uint32 color) = 0;
virtual void HorLine( int32 x, int32 y, int32 length, uint32 color) = 0;
virtual void VerLine( int32 x, int32 y, int32 length, uint32 color) = 0;
virtual void Rect( int32 x, int32 y, int32 lengthX, int32 lengthY, uint32 color) = 0;
// etc ...
protected:
DrawingMethod() {}; // private constructor: user must create children
static DrawingMethod* mInstance;
}
DrawingMethod has two child classes, DrawingMethodOpenGL and DrawingMethodSDL, that implement the drawing primitives using either OpenGL calls or by drawing to a SDLSurface pixelbuffer. At the start of my program, I make one of those children depending on the nature of the tool (main program -> OpenGL, small tool -> SDL) and SetInstance it as the active DrawingMethod. From that point on, I can use UI components without having to know if they're drawn with OpenGL or with software rendering. Of course the OpenGL calls have to behave the same as the framebuffer calls, so you have to switch off blending, the Z-buffer, etc. when drawing the OpenGL UI.
Page 6: Repaint or update. This is something I've experienced firsthand: my first UI had a "dirty" flag to signal components should be repainted, but checking this proved to be too complex (due to other stupid decisions of mine, such as giving a Text widget only a pointer to a string it should draw, with no way of knowing if that string has been changed since the last time it was drawn). Now I redraw everything each frame, modern graphic cards can draw hundreds of millions of polys each second, so a few ten-thousand lines are not going to make much difference. I'm only using software rendering for small tools, so redrawing those isn't much work either.
The laptop battery problem was new to me, but since I hardly ever use my laptop untethered, I'll happily ignore it for now :)
Page 7: Window classes: It's important to know who owns what, and how communication flows between components. My first UI attempt was way too simplistic: when something received a mouseclick, a simple callback function was called, and when something had to be displayed (a string or number f.e.), the UI component usually had a pointer to that variable.
The problem with the callback function is that it often needs to know a lot more than "button X has been clicked," such as the state of other widgets. This led to way too many global variables (nicely hidden in a class, but still ugly). Adding more parameters to the callback function only led to many specialized components that couldn't be reused anywhere else. Also, the code for any moderately complex arrangement of widgets is spread over many callback functions.
My current solution is to make UIBuilder classes which are responsible for both creating and managing UI components that interact. When a component is constructed, it gets a pointer to the class that created it. When the component is changed (f.e. when the user presses enter after editing a text field) the component notified its creator, who is then responsible for acting on the change and updating its other components.
The problem with the pointers-to-external-variables is that those variables may be deleted without the component being deleted. Also, you can't cache any info (such as the location of line breaks in a multi-line textfield) since any external changes can invalidate that cache.
I'm not sure if this is the best solution, but for now I give each component their own private data, that has to be accessed with Get/Set methods. This means I lose the automatic updates that happened when I changed the external variable, but it's far more reliable and clean.
Another problem is deleting an UI component that is currently being used. You press the OK button on your dialog, the dialog (including button) should disappear, but one of the buttons methods is on the call stack so you can't simply delete it.
Chaos uses a garbage collector to automatically remove components that are no longer referenced. This may very well be a superior approach, but it adds more complexity than I want at the moment. If you prefer to go this route, the details are on page 17 to 24. You may want to read the Wikipedia page on GC first, if you're new to the subject: http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)
For now I'm going to try delayed deletion: instead of deleting the button immediately, I'll add it to a list of objects that have to be deleted later, when it's safe to do so. Chaos runs his garbage collector after the event handling, this seems a good place to delete UI objects as well.
Page 8: How does a window work? The OnPaint() and OnKey() match my Draw() and CheckKey(), but my CheckMouse() call only gave info about a single click while Chaos' onDrag() provides info on clicking, dragging and releasing. Also, when the mouse is dragged, Chaos sends mouse events to the window that has the focus, even if the mouse moves outside it. My old UI only used focus for sending keypresses, not for mouse events, a mistake which made it quite impossible to implement dragging.
Page 12: Not so simple windows: Text edit controls are hard to do: there's cursor movement (arrows, home, end, page up/down), insert state (overwrite or insert), special actions like enter, backspace, delete or escape, function keys etc. Then there are whistles and bells such as copy&paste, selecting part of the text, undo support ...
But the first requirement of text controls is a font. Chaos uses windows functions to render fonts in a texture at startup, but for people who like to stay platform independent, there's the FreeType2 library. It's an opensource library that can parse a variety of fonts and render them to framebuffers, supports unicode, and has advanced features such as kerning. Get it from http://freetype.sourceforge.net/
The rest of the seminar was about other subjects such as his garbage collector method or large program design, about which I haven't anything to add except one small remark on page 16: Chaos advises not to include windows.h if it can be avoided, to keep buildtimes low. If you're an OpenGL programmers, you may be interested in Thatcher Ulrich's ogl.h include file, which only has those Win32 defines that OpenGL.h needs, so you can avoid including windows.h. It's in the public domain, so I've added it to the bonus pack.